Notes on Next.js

next dev 和 next build

Two Forms of Pre-rendering - Pre-rendering and Data Fetching | Learn Next.js

在开发模式下(当您运行 npm run devyarn dev 时),页面会根据每个请求进行预渲染。这也适用于静态生成,使其更易于开发。当投入生产时,静态生成将在构建时发生一次,而不是在每个请求时发生。

路由

Route handler

误区

How to call Route Handler within client component? · vercel/next.js · Discussion #58896 · GitHub

Fetching own API endpoint in React Server Components

数据获取

渲染

服务器组件

Learn Next.js: Static and Dynamic Rendering | Next.js

Rendering: Server Components | Next.js

静态渲染

Rendering: Server Components | Next.js

使用静态渲染,路线在构建时渲染,或者在数据重新验证后在后台渲染。结果被缓存并可以推送到内容交付网络 (CDN)。此优化允许您在用户和服务器请求之间共享渲染工作的结果。

动态渲染

Rendering: Server Components | Next.js

在 Next. js 中,您可以动态渲染包含缓存和未缓存数据的路由。这是因为 RSC 有效负载和数据是分开缓存的。这使您可以选择动态渲染,而不必担心在请求时获取所有数据对性能的影响。

动态渲染的组件可以使用 nextjs 的缓存加速渲染,这也是 nextjs 缓存存在的意义

nextjs 默认使用静态渲染渲染服务器组件,然而

在渲染过程中,如果发现动态函数或未缓存的数据请求,Next. js 将切换为动态渲染整个路由。

动态函数,如 cookies() , headers(), searchParams

未缓存的数据请求,包括,

Data Fetching: Fetching, Caching, and Revalidating | Next.js

The cache: no-store is added to fetch requests.

The revalidate: 0 option is added to individual fetch requests.

The fetch request is inside a Router Handler that uses the POST method.

The fetch request comes after the usage of headers or cookies.

The const dynamic = 'force-dynamic' route segment option is used.

The fetchCache route segment option is configured to skip cache by default.
The fetch request uses Authorization or Cookie headers and there’s an uncached request above it in the component tree.

客户端组件

Rendering: Client Components | Next.js

为了优化初始页面加载,Next. js 将使用 React 的 API 在服务器上为客户端和服务器组件呈现静态 HTML 预览。

预渲染

Two Forms of Pre-rendering - Pre-rendering and Data Fetching | Learn Next.js

静态生成是在构建时生成 HTML 的预渲染方法。然后,预渲染的 HTML 会在每个请求中重复使用。

服务器端渲染是在每个请求上生成 HTML 的预渲染方法。

关注 Static Generation with and without Data - Pre-rendering and Data Fetching | Learn Next.js

部分预渲染

Learn Next.js: Partial Prerendering (Optional) | Next.js

部分预渲染利用 React 的并发 API 并使用 Suspense 推迟应用程序的渲染部分,直到满足某些条件(例如加载数据)。

注: 来自 Suspense 的后备

后备与其他静态内容一起嵌入到初始静态文件中。在构建时(或重新验证期间),路线的静态部分被预渲染,其余部分被推迟,直到用户请求路线。

值得注意的是,将组件包装在 Suspense 中并不会使组件本身变得动态(记住您使用 unstable_noStore 来实现此行为),而是使用 Suspense 用作组件的静态部分和动态部分之间的路由边界。

部分预渲染的优点在于您无需更改代码即可使用它。只要您使用 Suspense 包装路线的动态部分,Next. js 就会知道路线的哪些部分是静态的,哪些部分是动态的。

next.config.js Options: Partial Prerendering (experimental) | Next.js

在没有 Parial Prerendering 之前,只有静态渲染,动态渲染,流式渲染

在渲染过程中,如果发现动态函数或未缓存的数据请求,Next. js 将切换为动态渲染整个路由。

这就意味着,一旦一条路由被确认为动态渲染,这条路线中的所有内容都不会在构建或者重新验证后在后台渲染,推送到 CDN, 共享渲染结果,而是会在请求的时候进行渲染,在请求的时候才利用 nextjs 的缓存加速渲染结果

这也是为什么我们需要 ppr, 尽可能提前渲染更多的内容,加速体验

如何启用
npm install next@canary
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: true,
  },
}

module.exports = nextConfig

缓存

Building Your Application: Caching | Next.js

请求记忆

React 扩展了 fetch API 以自动记住具有相同 URL 和选项的请求。这意味着您可以在 React 组件树中的多个位置调用相同数据的获取函数,而只需执行一次。

请求记忆是一项 React 功能,而不是 Next. js 功能。

记忆化仅适用于 fetch 请求中的 GET 方法。

对于 fetch 不适合的情况(例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),您可以使用 React cache 函数来记忆函数。

缓存会持续服务器请求的生命周期

缓存是非持久的,只存活于请求响应周期

数据缓存

Next. js 有一个内置的数据缓存,可以在传入的服务器请求和部署中保留数据获取的结果。这是可能的,因为 Next. js 扩展了本机 fetch API 以允许服务器上的每个请求设置自己的持久缓存语义。

缓存是持久的

如何退出

Data Fetching: Fetching, Caching, and Revalidating | Next.js

fetch requests are not cached if:

  • The cache: 'no-store' is added to fetch requests.
  • The revalidate: 0 option is added to individual fetch requests.
  • The fetch request is inside a Router Handler that uses the POST method.
  • The fetch request comes after the usage of headers or cookies.
  • The const dynamic = 'force-dynamic' route segment option is used.
  • The fetchCache route segment option is configured to skip cache by default.
  • The fetch request uses Authorization or Cookie headers and there’s an uncached request above it in the component tree.

全路由缓存

Next. js 的默认行为是在服务器上缓存路由的渲染结果(React 服务器组件负载和 HTML)。这适用于构建时或重新验证期间静态渲染的路线。

路由器缓存

当用户在路线之间导航时,Next. js 会缓存访问过的路线段并预取用户可能导航到的路线(基于视口中的 <Link> 组件)。
导航之间不会重新加载整页,并且会保留 React 状态和浏览器状态。
会话:缓存在整个导航过程中持续存在。但是,它会在页面刷新时被清除

Functions

fetch

Functions: fetch | Next.js

在浏览器中, cache 选项指示获取请求将如何与浏览器的 HTTP 缓存交互。通过此扩展, cache 指示服务器端获取请求将如何与框架的持久 HTTP 缓存交互。

(default) - Next. js looks for a matching request in its Data Cache.
force-cache (默认)- Next. js 在其数据缓存中查找匹配的请求。

  • 如果存在匹配并且是新鲜的,则将从缓存中返回。
  • 如果没有匹配项或匹配项过时,Next. js 将从远程服务器获取资源并使用下载的资源更新缓存。
非显式使用的 fetch

一些第三库的背后实际上是使用 fetch 发出请求, 没有指定 relivate, 默认为 undefined, 经过 patch-fetch 后为 false, 指定了 noStore 的情况下为 0

例如: @vercel/postgre

unstable_noStore

Functions: unstable_noStore | Next.js

unstable_noStore 可用于以声明方式选择退出静态渲染并指示不应缓存特定组件。

unstable_noStore 相当于 fetch 上的 cache: 'no-store'

unstable_noStore 优于 export const dynamic = ‘force-dynamic’ ,因为它更细粒度并且可以在每个组件的基础上使用

latest 中,如果路线中存在 unstable_noStore ,就会退出静态渲染,转为动态渲染整个路由,因为这相当于在路线中出现了未能缓存的数据请求,等价与路由段配置的 force-dynamic 或者 fetch 中显式 no-store or revalidate: 0

canary 中, 如果开启了 ppr, 当你在组件中 suspense 边界内组件中声明 noStore 的时候,这个组件就会退出预渲染.

值得注意的是, 在当前版本的实现中, 它会使得后续所有的 fetch 请求的 revalidate 设置为 0, 并导致一部分使用 POST 进行 fetch 请求的组件退出预渲染并渲染失败, 疑似 bug

在当前版本的实现中, 使用 noStore 就意味着退出 nextjsdata cache


unstable_cache

Functions: unstable_cache | Next.js

unstable_cache 允许您缓存昂贵操作的结果(例如数据库查询),并在多个请求中重用它们。

  • 不能缓存 revalidate === 0 的请求
// unstable-cache.ts
if (options.revalidate === 0) {
throw new Error(
`Invariant revalidate: 0 can not be passed to unstable_cache(), must be "false" or "> 0" ${cb.toString()}`,
);
}
unstable_cache 和 fetch

fetch 缓存的是 fetch 请求的返回值, 而 unstable_cache 缓存的是给定函数体的返回值

unstable_cache 和 unstable_noStore

unstable_cache 里使用 unstable_noStore 没有任何问题,

因为什么都不会发生

// unstable_noStore.ts
export function unstable_noStore() {
...
else {
store.isUnstableNoStore = true
markCurrentScopeAsDynamic(store, callingExpression)
}
}

// dynamic-rendering.ts
export function markCurrentScopeAsDynamic(
store: StaticGenerationStore,
expression: string,
): void {
...
if (store.isUnstableCacheCallback) {
// inside cache scopes marking a scope as dynamic has no effect because the outer cache scope
// creates a cache boundary. This is subtly different from reading a dynamic data source which is
// forbidden inside a cache scope.
return;
}
}

我当时说的 unstable_cache 梭哈指的是全部使用 unstable_noStore + unstable_cache ,

放弃在其中一个组件开启 unstable_noStore 的时候, 预渲染另一个带有 db 操作的组件的幻想

你的意思是,

  • 在需要预渲染的组件里用 unstable_cache 缓存 db 函数, 不使用 unstable_noStore

  • 在需要动态渲染的组件里用 unstable_noStore

这样就可以了?

说实话我很质疑… 不过 unstable_cache 的源码部分我没怎么看, 因为没测试这一部分, 主要精力直接放在不加 unstable_cache 就可以预渲染,

因为很明显, 没理由在你需要预渲染的组件里加上 unstable_cache, 不符合逻辑和语义

所以我觉得你可能碰上了我昨天说的第二个 bug, 你的本地缓存里意外出现了你想缓存的数据, 我建议你删掉 .next 重新 build 试试


很多问题, 我暂时也还是一知半解, 毕竟, 源码真的很大还都是屎山, 一个 patch-fetch 写 600 行…

我只能先讲一下, 我解决这个问题的思路

Error connecting to database: Route /dashboard needs to bail out of prerendering at this point because it used revalidate: 0. React throws this special object to indicate where. It should not be caught by your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error

万恶之源是这个报错, 应该很熟悉了吧, 这个报错来自

//dynamic-rendering.ts
function postponeWithTracking(
prerenderState: PrerenderState,
expression: string,
pathname: string
): never {
const reason =
`Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` +
`React throws this special object to indicate where. It should not be caught by ` +
`your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`
...
React.unstable_postpone(reason)
}

关于 React 的 Postpone API 可以参考

Add Postpone API by sebmarkbage · Pull Request #27238 · facebook/react · GitHub

预渲染的函数调用路线

entry-base > patchFetch > trackFetchMetric >

动态渲染的函数调用路线

entry-base > patchFetch > unstable_noStore > > markCurrentScopeAsDynamic > postponeWithTRracking

万恶之源报错时的错误路线

entry-base > patchFetch > trackDynamicFetch > postponeWithTracking


nextjs 在 build 的时候,会用一个 AsyncLocalStorage 的实例保存某条路线的元信息

如果组件内部声明了 unstable_noStore(),那么,就由这个 noStore 函数负责后续操作,而且这个组件会先被处理

糟糕的地方在于 unstable_noStore() 函数内部,直接获取 Storage,然后用点号赋值,store.isUnstableNoStore = true,

更糟糕的地方在于这个 Storage 似乎是被单个路线所有组件共用的。

而且还有一个函数 patchFetch,每次渲染组件都会调用,根据你的 store.isUnstableStore,修改你的 store.revalidate 值(db 操作默认为 undefined)

// patch-fetch.ts/patchFetch
const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore
...

if (typeof revalidate === 'undefined') {
...
if (isUsingNoStore) {
revalidate = 0
cacheReason = 'noStore call'
} else {
cacheReason = 'auto cache'
revalidate =
typeof staticGenerationStore.revalidate === 'boolean' ||
typeof staticGenerationStore.revalidate === 'undefined'
? false
: staticGenerationStore.revalidate
}
} else if (!cacheReason) {
cacheReason = `revalidate: ${revalidate}`
}

值得注意的是, 使用 unstable_noStore 的组件不会走到这一步, 只有没有声明 noStore 的才会

所以,一开始,单独 build 的时候

  • 第一个组件是 isUnstableStoretrue,有 noStore 函数,直接就走动态渲染

  • 第二个组件是 isUnstableStorefalse,但是 {cacheReason:auto cache,revalidate:false},数据是可以 cache 的, 调用 trackFetchMetric, 所以走预渲染

如果组件一起渲染的话

第二个组件被坑了,isUnstableStore:true ,但是 { revalidate : 0,cacheReason : 'noStore call'}

由于 revalidate === 0

//patch-fetch.ts/patchFetch
if (revalidate === 0) {
trackDynamicFetch(staticGenerationStore, 'revalidate: 0');
}

会调用 trackDynamicFetch, 而且 prerenderStatetrue, 会导致我们调用 postponeWithTracking, 并调用 React.unstable_postpone(reason), 最终报错

// dynamic-rendering.ts/trackDynamicFetch
export function trackDynamicFetch(
store: StaticGenerationStore,
expression: string,
) {
if (store.prerenderState) {
postponeWithTracking(store.prerenderState, expression, store.urlPathname);
}
}

注意, store.sprerenderState 表示我们在 ppr 模式下, 处于构建中, 参考

// dynamic-rendering.ts/markCurrentScopeAsDynamic
if (
// We are in a prerender (PPR enabled, during build)
store.prerenderState
) {
// We track that we had a dynamic scope that postponed.
// This will be used by the renderer to decide whether
// the prerender requires a resume
postponeWithTracking(store.prerenderState, expression, pathname);
}

为什么动态渲染最后也会调用 postponeWithTracking, 却没有报错?

// dynamic-rendering.ts/postponeWithTracking

prerenderState.dynamicAccesses.push({

// When we aren't debugging, we don't need to create another error for the
// stack trace.
stack: prerenderState. isDebugSkeleton ? new Error(). stack : undefined,
expression,
});



因为被 unstable_noStore 函数调用后的 store, prerenderState 长这样

// postponeWithTracking prerenderState
{
isDebugSkeleton: undefined,
dynamicAccesses: [
{ stack: undefined, expression: 'unstable_noStore()' },
{ stack: undefined, expression: 'unstable_noStore()' }
]
}
// unstable-no-store. ts
const callingExpression = 'unstable_noStore()';
markCurrentScopeAsDynamic (store, callingExpression);

而调用 trackDynamicFetchstore, prerenderState 最终长这样

// postponeWithTracking prerenderState
{
isDebugSkeleton: undefined,
dynamicAccesses: [
{ stack: undefined, expression: 'unstable_noStore()' },
{ stack: undefined, expression: 'revalidate: 0' },
{ stack: undefined, expression: 'unstable_noStore()' },
{ stack: undefined, expression: 'revalidate: 0' }
]}

推测: React. unstable_postpone (reason) 接收的 reason 里不能有 revalidate:0

具体实现参考 React/packages/react/src/ReactPostpone.js 还有上面上面提过的 github pull


Functions: unstable_cache | Next.js

unstable_cache 允许您缓存昂贵操作的结果(例如数据库查询),并在多个请求中重用它们。

import { unstable_cache} from 'next/cache';

const getCachedRevenue = unstable_cache (
async () => fetchRevenue(),
['Revenue']
);

export default function App(){
const revenue = await getCachedRevenue()
}

unstable_cacheReact cache 的缓存持久版本

unstable_cache 的缓存是存放在服务端的

动态函数

Rendering: Server Components | Next.js 动态函数依赖于只能在请求时获知的信息,例如用户的 cookie、当前请求标头或 URL 的搜索参数。在 Next. js 中,这些动态函数是:

  • cookies()headers() :在服务器组件中使用它们将在请求时选择整个路由进行动态渲染。
  • searchParams :使用 Pages 属性将选择页面在请求时进行动态渲染。

解决方案

临时解决方案:

dynamic-rendering. ts/markCurrentScopeAsDynamic

调用 postponeWithTracking (store. prerenderState, expression, pathname); 之前加上 store. isUnstableNoStore = false

最终解决方案

我对 AsyncLocalStorage 还有 nextjs 整体的理解还有待加强, 所以上面只能作为临时方案

Markdown

Markdown & MDX | Docs

nextjs 使用 remark 和 rehype 作为 markdown 的解析

你可能需要 remark-gfmrehype-slug

GitHub - remarkjs/remark-gfm: remark plugin to support GFM (autolink literals, footnotes, strikethrough, tables, tasklists)

GitHub - rehypejs/rehype-slug: plugin to add `id` attributes to headings

next/canary - Partial Prerendering

unstable_noStore

unstable_noStore 以声明方式选择退出静态渲染

Static Bail Out Caught | Next.js

启用部分预渲染 (PPR) 后,使用选择动态渲染的 API,例如 cookiesheadersfetch (例如 cache: 'no-store' )将导致 React 抛出一个特殊的错误对象

页面中没有使用 unstable_noStore 的组件会被预渲染,前提是这些组件中不包括动态渲染的 API, 即动态函数。

  • 确保您没有将选择动态渲染的 Next. js API 包装在 try/catch 块中。
  • 如果您确实将这些 API 包装在 try/catch 中,请确保重新抛出原始错误,以便 Next. js 可以捕获它。
  • 或者,在 try/catch 之前插入 unstable_noStore()

带有数据库操作的 Partial Prerendering

使用第三方库在服务器上获取数据

如果您使用的第三方库不支持或公开 fetch (例如数据库、CMS 或 ORM 客户端),您可以配置这些库的缓存和重新验证行为使用 Route Segment Config Option 和 React 的 cache 函数来请求。

数据是否被缓存取决于路线段是静态渲染还是动态渲染。如果该段是静态的(默认),则请求的输出将作为路由段的一部分进行缓存和重新验证。如果该段是动态的,则请求的输出将不会被缓存,并且会在渲染该段时根据每个请求重新获取。

您还可以使用实验性 unstable_cache API。

unstable_cache

Functions: unstable_noStore | Next.js

在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。

为了解决 sql 的 revalidate 0, 我们引入了 unstable_cache 。

对于 ppr 和 sql revalidate 0 的问题,如果真的有想要完全静态的组件还带有 db 查询,
我的建议是 unstable_cache 梭哈,不设置过期时间,算是一种半静态吧,除了第一次比较慢,后面其他用户第二次访问就很快了

,借助这一点我们就可以 ppr 数据查询操作了,

正如我前面所说,这事儿跟 cache 没什么关系,cache 只是为了降低动态组件查询数据库的频率,在静态路线里写不写 cache 没什么意义,是同一个结果

  1. 在 ppr,
    如果我们在 suspense 边界内部声明了 noStore, 根据约定,suspense 内部应该是一个动态组件,内容不应该被 ppr
    如果我们的组件没有声明 noStore, 却使用了 db 操作,我们猜测 db 操作背后是 revalidate 0 ,nextjs 会表现出和正式版不一样的操作,它会很困惑,不知道我们到底要做什么,我觉得这里就是一切困惑的来源。ppr 无法和 revalidate 0 共存,可以提 issue 。
    好在官方可能考虑过这一点了,如果你在 unstable_cache 缓存的函数中声明 noStore, 那么在 nextjs 看来,组件仍然是可以被 ppr 的,不会受到 nosStore 和 revalidate 0 影响了

前提:canary 但是 config 里面注释掉 ppr = 正式版

我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。我觉得这个就是 ppr 了对吗? canary 的支持只不过让这个实现更加简单了一点而已:不需要 cache 方法包裹,而是没有使用 noStore 就默认为 cache 。

ppr 指的是部分预渲染,相当于默认一切都是静态的,只要 suspense 里面没有 no store, 但是这里有坑,最后说。

在正式版/canary without ppr 中,
这个 noStore 是不是 cache 的 callback, 都没有关系,只要你这个路线中,任意一个地方出现了 noStore, next 在 build 的时候,就等价于路由段配置中的 force-dynamic 或者 fetch 中的 no-store, 相当于退出静态渲染,改用动态。

当你刷新的时候,为什么另外两个组件都去获取数据了,有 cache 的却没反应呢,因为 unstable_cache 它 cache 的函数的返回值。

所以这跟 ppr 有什么关系?重要的是要把 ppr 和 cache 分开。

你加 cache, 只是为了优化,降低它查询数据库的频率。

部分预渲染中组件渲染的全过程

动态组件也要经过服务器才能查询 db ,或者 fetch 相关数据, 不可能把 db 相关操作放到客户端来进行

这也就意味着他们可以使用

  1. fetch 带来的持久缓存
  2. 每次 req-res 周期中存在的 rect Request Memoization
    3. unstable-cache

但是动态组件并不能使用全路由缓存

Building Your Application: Caching | Next.js

Next. js 的默认行为是在服务器上缓存路由的渲染结果(React 服务器组件负载和 HTML)。这适用于构建时或重新验证期间静态渲染的路线。

您可以选择退出完整路由缓存,或者换句话说,为每个传入请求动态渲染组件

其他参考

caching - Disable cache for a @vercel/postgres query in NextJS using the app router - Stack Overflow

Deep Dive: Caching and Revalidating · vercel/next.js · Discussion #54075 · GitHub

pnpm

#Todo --use-npm?

Could not locate the repository for “https://github.com/vercel/next-learn/tree/main/dashboard/starter-example”. Please check that the repository exists and try again.

proxychains pnpm dlx create-next-app@latest nextjs-dashboard  --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"

Markdown

`‘@next/mdx’` not support ESM import bug ❌ · Issue #43665 · vercel/next.js · GitHub

”error createContext only works in Client Components” still exists · Issue #60877 · vercel/next.js · GitHub

MDX support width appDir enabled · Issue #47523 · vercel/next.js · GitHub

MDX in app router causes - “error createContext only works in Client Components” · Issue #50110 · vercel/next.js · GitHub

[[Notes on Next.js]]

Notes on Next.js